import { NesUtil } from '../NesUtil';
import { NesAudio } from './NesAudio';

export interface NesPlayerHankAddressInfo {
    address: number;
    value: number;
}

export interface NesPlayerHankInfo {
    name: string;
    enable: boolean;
    value: NesPlayerHankAddressInfo[];
}

export interface NesPlayerGameHankInfo {
    game: string;
    value: NesPlayerHankInfo[];
}

export interface NesPlayerOption {
    canvas: HTMLCanvasElement;
    zoom?: number;
    frames?: number;
    hack?: NesPlayerGameHankInfo[];
}

export class NesPlayer {
    private zoom = 4;
    private frames = undefined;

    private originWidth = 256;
    private originHeight = 240;

    private width = this.originWidth * this.zoom;
    private height = this.originHeight * this.zoom;

    private canvas: HTMLCanvasElement;
    private context: CanvasRenderingContext2D;
    private audio: NesAudio;
    public readonly nesUtil: NesUtil;

    private image: ImageData;
    private buffer_u8: Uint8ClampedArray;
    private buffer_32: Uint32Array;
    private hack?: NesPlayerGameHankInfo[];

    constructor(option: NesPlayerOption) {
        const { originWidth, originHeight } = this;

        const canvas = this.canvas = option.canvas;

        if (option.zoom) {
            this.zoom = option.zoom;
            this.width = this.originWidth * this.zoom;
            this.height = this.originHeight * this.zoom;
        }

        if (option.frames) {
            this.frames = option.frames;
        }

        const { width, height } = this;

        canvas.width = width;
        canvas.height = height;

        const context = this.context = canvas.getContext("2d");
        context.imageSmoothingEnabled = false;

        this.image = context.getImageData(0, 0, width, height);
        const buffer = new ArrayBuffer(this.image.data.length);
        this.buffer_u8 = new Uint8ClampedArray(buffer);
        this.buffer_32 = new Uint32Array(buffer);

        this.audio = new NesAudio();

        const nesUtil = this.nesUtil = new NesUtil({
            writeFrame: (buffer: number[]) => {
                for (let x = 0; x < originWidth; x++) {
                    for (let y = 0; y < originHeight; y++) {
                        const index = x + originWidth * y;
                        this.setColor(x, y, buffer[index]);
                    }
                }
                this.image.data.set(this.buffer_u8);
                this.context.putImageData(this.image, 0, 0);
            },

            updateStatus(text: string) {
                console.log(text);
            },

            onAudioSample: (l, r) => {
                this.audio.setAudioSample(l, r);
            },
        });

        if (option.hack) {
            this.hack = option.hack;
        }
    }


    setColorByPos(pos: number, pixel: number) {
        this.buffer_32[pos] = 0xFF000000 | pixel;
    }

    setColor(x: number, y: number, pixel: number) {
        const { zoom, width } = this;
        const posX = x * zoom;
        const posY = y * zoom;
        for (let offsetX = 0; offsetX < zoom; offsetX++) {
            for (let offsetY = 0; offsetY < zoom; offsetY++) {
                const x = posX + offsetX;
                const y = posY + offsetY;
                const pos = x + y * width;
                this.setColorByPos(pos, pixel);
            }
        }
    }

    run() {
        if (this.frames) {
            setInterval(() => {
                this.nesUtil.frame();
            }, 1000 / this.frames);
        } else {
            requestAnimationFrame(() => {
                this.nesUtil.frame();
                this.run();
            });
        }
    }

    async load(game: string) {
        return fetch(`./roms/${game}.nes`).then(async (res) => {
            return res.arrayBuffer().then(buffer => {
                this.nesUtil.loadRom(new Uint8Array(buffer));
                this.run();
                if (this.hack) {
                    const findHack = this.hack.find(info => info.game === game);
                    if (findHack) {
                        this.nesUtil.cpu.hack = findHack.value;
                    }
                }
                return this;
            });
        });
    }
}
